﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Globalization;
using System.Reflection;
using System.Drawing;

namespace GBPVRLibrary.MetadataLibrary
{
    public class MetadataLookup
    {
        private XDocument Configuration { get; set; }
        private List<MediaType> MediaTypes { get; set; }

        public MetadataLookup()
        {
#if(DEBUG)
            System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
            System.IO.Stream stream = assembly.GetManifestResourceStream("GBPVRLibrary.MetadataLibrary.GBPVRMetadataConfiguration.xml");
            this.Configuration = XDocument.Load(new System.Xml.XmlTextReader(stream));
#else
            System.Reflection.Assembly assembly = System.Reflection.Assembly.GetExecutingAssembly();
            System.IO.Stream stream = assembly.GetManifestResourceStream("GBPVRLibrary.MetadataLibrary.GBPVRMetadataConfiguration.xml");
            this.Configuration = XDocument.Load(new System.Xml.XmlTextReader(stream));
#endif
            this.MediaTypes = (from md in this.Configuration.Root.Element("MediaTypes").Elements("MediaType")
                               select new MediaType()
                               {
                                   Name = md.Attribute("name").Value,
                                   Sources = (from s in md.Element("Sources").Elements("Source")
                                              select new Source()
                                              {
                                                  Name = s.Attribute("name").Value,
                                                  Type = s.Attribute("type").Value,
                                                  Search = new SourceSearch()
                                                  {
                                                      Url = s.Element("Search").Element("Url").Value,
                                                      ResultsRegex = ParseRegex(s.Element("Search").Element("ResultsRegex")),
                                                      ResultsExclude = ParseRegex(s.Element("Search").Element("ResultsExclude")),
                                                      ResultExactMatch = ParseRegex(s.Element("Search").Element("ResultExactMatch")),
                                                      ResultTitleRegex = ParseRegex(s.Element("Search").Element("Result").Element("TitleRegex")),
                                                      ResultUnitRegex = ParseRegex(s.Element("Search").Element("Result").Element("UnitRegex")),
                                                      ThumbnailRegex = ParseRegex(s.Element("Search").Element("Result").Element("ThumbnailRegex")),
                                                      ResultUrlRegex = ParseRegex(s.Element("Search").Element("Result").Element("Url").Element("UrlRegex")),
                                                      ResultUrlRegexReplacement = ParseRegex(s.Element("Search").Element("Result").Element("Url").Element("UrlRegexReplacement")),
                                                      ReplaceUrlWith = s.Element("Search").Element("Result").Element("Url").Element("UrlRegexReplacement") != null ? s.Element("Search").Element("Result").Element("Url").Element("UrlRegexReplacement").Attribute("replaceWith").Value : "",
                                                      ResultUrlLocation = s.Element("Search").Element("Result").Element("Url").Element("UrlLocation").Value
                                                  },
                                                  Attributes = ContentAttribute.LoadAttributes(s.Element("Content"))
                                              }
                                              ).ToList()
                               }
                               ).ToList();
        }

        public List<string> Types
        {
            get
            {
                return (from mt in this.MediaTypes
                        select mt.Name).ToList();
            }
        }

        public List<string> Sources(string MediaType)
        {
            return (from Source s in
                        (from mt in this.MediaTypes where mt.Name == MediaType select mt.Sources).FirstOrDefault()
                    select s.Name).ToList();
        }

        /// <summary>
        /// Does an internet search and loads all the results
        /// NOTE: this is a synchronous call, so it is recommend this is done on a seperate thread to the gui thread
        /// </summary>
        /// <param name="MediaTypeName">the mediatype</param>
        /// <param name="SourceName">the source</param>
        /// <param name="SearchText">the search text</param>
        /// <returns>a list of search results</returns>
        public List<MetadataLookupSearchResult> SearchMediaTypeSource(string MediaTypeName, string SourceName, string SearchText)
        {
            Source source = (from Source s in (from mt in this.MediaTypes where mt.Name == MediaTypeName select mt.Sources).FirstOrDefault() where s.Name == SourceName select s).FirstOrDefault();
            if (source == null)
                throw new Exception(String.Format("Unknown source '{0}' for MediaType '{1}'", SourceName, MediaTypeName));
            return source.Search.LoadResults(SearchText, source);
        }

        internal static Regex ParseRegex(XElement Element)
        {
            if (Element == null)
                return null;
            if(Element.Attribute("options") == null)
                return new Regex(Element.Value);
            RegexOptions options = (RegexOptions)int.Parse(Element.Attribute("options").Value);            
            return new Regex(Element.Value, options);
        }

        public object LoadItem(string MediaTypeName, string SourceName, string Url, Type MetadataType)
        {
            Source source = (from Source s in (from mt in this.MediaTypes where mt.Name == MediaTypeName select mt.Sources).FirstOrDefault() where s.Name == SourceName select s).FirstOrDefault();
            if (source == null)
                throw new Exception(String.Format("Unknown source '{0}' for MediaType '{1}'", SourceName, MediaTypeName));

            string PageSource = Url;
            DateTime start = DateTime.Now;
            if(Uri.IsWellFormedUriString(Url, UriKind.RelativeOrAbsolute)) // only load it if its actually an url
                PageSource = WebSiteHelper.ReadGetResponse(ref Url, null);
            TimeSpan taken = DateTime.Now.Subtract(start);
            if (String.IsNullOrEmpty(PageSource))
                return null;

            object result = Activator.CreateInstance(MetadataType);
            Dictionary<string, System.Reflection.PropertyInfo> properties = new Dictionary<string, System.Reflection.PropertyInfo>();
            foreach (System.Reflection.PropertyInfo info in MetadataType.GetProperties())
            {
                if((from att in info.GetCustomAttributes(false) select att.GetType()).Contains(typeof(MetadataAttribute)))
                    properties[info.Name] = info;
            }
            foreach (ContentAttribute attribute in source.Attributes)
            {
                if (!properties.ContainsKey(attribute.Name))
                    continue;
                properties[attribute.Name].SetValue(result, attribute.LoadAttribute(Url, PageSource), null);
            }
            taken = DateTime.Now.Subtract(start);
            return result;
        }
    }

    public class MetadataLookupSearchResult
    {
        public string Title { get; set; }
        public string Unit { get; set; }
        public string Url { get; set; }
        public string ThumbnailUrl { get; set; }
    }

    public class MediaType
    {
        public string Name { get; set; }
        public List<Source> Sources { get; set; }

    }

    public class Source
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public SourceSearch Search { get; set; }
        public List<ContentAttribute> Attributes { get; set; }
    }

    public class ContentAttribute
    {
        public string Name { get; set; }
        public Regex ValueRegex { get; set; }
        public Regex ContentRegex { get; set; }
        public List<Replacement> Replacements { get; set; }
        public string UrlPostfix { get; set; }
        public string UrlPrefix { get; set; }
        public string Format { get; set; }
        public string Type { get; set; }
        public string ParentType { get; set; }
        public bool ContentIsUrl { get; set; }
        public string Postfix { get; set; }
        public string Prefix { get; set; }

        public List<ContentAttribute> Attributes { get; set; }

        public static List<ContentAttribute> LoadAttributes(XElement Element)
        {
            if (Element == null)
                return null;
            return (from att in Element.Elements("Attribute")
                    select new ContentAttribute()
                    {
                        Name = att.Attribute("name").Value,
                        ValueRegex = att.Element("Value") != null && att.Element("Value").Element("ContentRegex") != null ? MetadataLookup.ParseRegex(att.Element("Value").Element("ContentRegex")) : (MetadataLookup.ParseRegex(att.Element("Value")) ?? MetadataLookup.ParseRegex(att)),
                        ContentRegex = MetadataLookup.ParseRegex(att.Element("ContentRegex")),
                        UrlPrefix= att.Element("ContentRegex") != null && att.Element("ContentRegex").Attribute("urlPrefix") != null ? att.Element("ContentRegex").Attribute("urlPrefix").Value : null,
                        ContentIsUrl = att.Element("ContentRegex") != null && att.Element("ContentRegex").Attribute("isUrl") != null ? bool.Parse(att.Element("ContentRegex").Attribute("isUrl").Value) : false,
                        ParentType = att.Attribute("type") != null ? att.Attribute("type").Value : "",
                        Type = att.Element("Value") == null ? (att.Attribute("type") == null ? "String" : att.Attribute("type").Value) : (att.Element("Value").Attribute("type") == null ? "String" : att.Element("Value").Attribute("type").Value),
                        UrlPostfix = att.Attribute("urlPostfix") == null ? null : att.Attribute("urlPostfix").Value,
                        Format = att.Attribute("format") == null ? null : att.Attribute("format").Value,
                        Postfix = att.Attribute("postfix") == null ? null :att.Attribute("postfix").Value,
                        Prefix = att.Attribute("prefix") == null ? null : att.Attribute("prefix").Value,
                        Replacements = (att.Element("Value") == null || att.Element("Value").Element("Replacements") == null) ? null :
                                        (from r in att.Element("Value").Element("Replacements").Elements("Replacement") select new Replacement()
                                        {
                                            Expression = MetadataLookup.ParseRegex(r),
                                            ReplaceWith = r.Attribute("replaceWith") == null ? "" : r.Attribute("replaceWith").Value
                                        }).ToList(),
                        Attributes = LoadAttributes(att.Element("Value"))
                    }).ToList();
        }

        private Type LoadType(string Name)
        {
            Type type = System.Type.GetType(Name);
            if(type == null)
                type = System.Type.GetType(String.Format("System.{0}", Name));
            if (type == null)
                type = System.Type.GetType(String.Format("{0}.{1}", this.GetType().Namespace, Name));
            if (type == null && Name == "Image")
                return typeof(System.Drawing.Image);
            return type;                
        }

        public object LoadAttribute(string Url, string Content)
        {
            if (!String.IsNullOrEmpty(UrlPostfix))
            {
                string url = Url + UrlPostfix;
                Content = WebSiteHelper.ReadGetResponse(ref url, null);
            }

            // if the contentregex isnt null, we must need to filter out the "rubbish" and only concentrate on the content we are looking for
            if (ContentRegex != null)
            {
                Match contentMatch = ContentRegex.Match(Content);
                if (!contentMatch.Success)
                    return null;
                Content = contentMatch.Value;
            }

            if (ContentIsUrl)
            {
                string url = (String.IsNullOrEmpty(UrlPrefix) ? "" : UrlPrefix) + Content;
                Content = WebSiteHelper.ReadGetResponse(ref url, null);
            }

            if (String.IsNullOrEmpty(Content))
                return null;

            Type type = LoadType(this.Type);
            if (this.Attributes != null && this.Attributes.Count > 0)
            {
                // we have to create a generic list since we don't know what type is until runtime
                object values = CreateGenericList(type);
                // have to use a methodinfo to add the objects aswell
                MethodInfo valuesAdd = values.GetType().GetMethod("Add");
                foreach (Match match in this.ValueRegex.Matches(Content))
                    valuesAdd.Invoke(values, new object[] { LoadAttributeType(type, this.Attributes, Url, match.Value) });
                return values;
            }

            MatchCollection matches = ValueRegex.Matches(Content);
            if (matches.Count == 0)
                return null;
            List<object> results = new List<object>();
            foreach (Match match in matches)
            {
                string value = match.Value;
                if (!String.IsNullOrEmpty(this.Prefix))
                    value = this.Prefix + value;
                if (!String.IsNullOrEmpty(this.Postfix))
                    value += this.Postfix;
                if (Replacements != null && Replacements.Count > 0)
                {
                    foreach (Replacement replacement in Replacements)
                        value = replacement.Expression.Replace(value, replacement.ReplaceWith ?? "");
                }
                if (this.Type == "DateTime")
                {
                    // parse the date
                    try
                    {
                        results.Add(DateTime.ParseExact(WebSiteHelper.RemoveHTML(value).Trim(), Format, new CultureInfo("en-us")));
                    }
                    catch (Exception) { }
                }
                else if (this.Type == "TimeSpan")
                {
                    try
                    {
                        string stime = WebSiteHelper.RemoveHTML(value).Trim();
                        if (Format == "%m") // minute
                            return new TimeSpan(0, int.Parse(stime), 0);
                        DateTime time = DateTime.ParseExact(stime, Format, new CultureInfo("en-us"));

                        results.Add(time.TimeOfDay);
                    }
                    catch (Exception) {  }
                }
                else if (this.Type == "Number")
                {
                    results.Add(Evaluator.EvalToDouble(WebSiteHelper.RemoveHTML(value).Trim()));
                }
                else if (this.Type == "Enum")
                {
                    results.Add((int)Evaluator.EvalToDouble(WebSiteHelper.RemoveHTML(value).Trim()));
                }
                else if (this.Type == "Image")
                {
                    // download the image
                    results.Add(WebSiteHelper.DownloadImage(WebSiteHelper.RemoveHTML(value).Trim()));
                }
                else if (this.Type == "String")
                {
                    results.Add(WebSiteHelper.RemoveHTML(value));
                }
                else
                {
                    // its a custom type.
                    return LoadAttributeType(type, this.Attributes, Url, Content);
                }
            }
            if (ParentType != "List") // only expecting a single value
                return results.Count > 0 ? results[0] : null;
            // cast the list to the correct type
            switch (this.Type)
            {                
                case "DateTime":
                    return (from DateTime? dt in results where dt != null select dt).ToList();
                case "TimeSpan":
                    return (from TimeSpan? ts in results where ts != null select ts).ToList();
                case "Number":
                    return (from double? num in results where num != null select num).ToList();
                case "Image":
                    return (from Image img in results where img != null select img).ToList();
                default: // Strings
                    return (from string str in results where str != null select str).Distinct().ToList();
            }
        }

        private object LoadAttributeType(Type Type, List<ContentAttribute> Attributes, string Url, string PageSource)
        {
            object result = Activator.CreateInstance(Type);
            Dictionary<string, System.Reflection.PropertyInfo> properties = new Dictionary<string, System.Reflection.PropertyInfo>();
            foreach (System.Reflection.PropertyInfo info in Type.GetProperties())
            {
                if ((from att in info.GetCustomAttributes(false) select att.GetType()).Contains(typeof(MetadataAttribute)))
                    properties[info.Name] = info;
            }
            foreach (ContentAttribute attribute in Attributes)
            {
                if (!properties.ContainsKey(attribute.Name))
                    continue;
                properties[attribute.Name].SetValue(result, attribute.LoadAttribute(Url, PageSource), null);
            }
            return result;
        }

        /// <summary>
        /// Creates a List&lt;Type&gt; and returns it as a Object.
        /// This is helpful when you don't know the type of List&lt;T&gt; you want until runtime
        /// </summary>
        /// <param name="Type">the type of objects the list should contain</param>
        /// <returns>an instance of  List&lt;Type&gt;</returns>
        private Object CreateGenericList(Type Type)
        {
            Type listType = typeof(List<>);
            Type[] typeArgs = { Type };
            Type genericType = listType.MakeGenericType(typeArgs);
            object o = Activator.CreateInstance(genericType);
            return o;
        }

    }

    public class Replacement
    {
        public Regex Expression { get; set; }
        public string ReplaceWith { get; set; }
    }

    public class SourceSearch
    {
        public string Url { get; set; }
        public Regex ResultsRegex { get; set; }
        public Regex ResultExactMatch { get; set; }
        public Regex ResultsExclude { get; set; }
        public Regex ResultTitleRegex { get; set; }
        public Regex ResultUnitRegex { get; set; }
        public Regex ResultUrlRegex { get; set; }
        public Regex ResultUrlRegexReplacement { get; set; }
        public string ReplaceUrlWith { get; set; }
        public Regex ThumbnailRegex { get; set; }
        public string ResultUrlLocation { get; set; }

        public List<MetadataLookupSearchResult> LoadResults(string SearchText, Source source)
        {
            List<MetadataLookupSearchResult> results = LoadResultsActual(MetadataFinder.MetadataSearchText(SearchText), source);
            if (results == null || results.Count == 0)
                results = LoadResultsActual(SearchText, source);
            return results;
        }

        private List<MetadataLookupSearchResult> LoadResultsActual(string SearchText, Source source)
        {

            string url = String.Format(this.Url, HttpUtility.UrlEncode(SearchText));
            string webpage = WebSiteHelper.ReadGetResponse(ref url, null);
            if (String.IsNullOrEmpty(webpage))
                throw new Exception("Failed to read in search page, if this continues to help please report the problem");
            List<MetadataLookupSearchResult> results = new List<MetadataLookupSearchResult>();

            // check if its an exact match
            if (ResultExactMatch != null && ResultExactMatch.IsMatch(webpage))
            {
                string title = SearchText;
                foreach (ContentAttribute attribute in source.Attributes)
                    if (attribute.Name == "Title")
                        title = attribute.LoadAttribute(url, webpage) as string;

                results.Add(new MetadataLookupSearchResult()
                {
                    Title = String.IsNullOrEmpty(title) ? SearchText : title,
                    Url = url
                });
                return results;
            }
            List<string> FoundUrls = new List<string>();
            if (ResultsRegex.Matches(webpage).Count == 0)
            {
                url = String.Format(this.Url, HttpUtility.UrlEncode(SearchText));
                webpage = WebSiteHelper.ReadGetResponse(ref url, null);
            }
            foreach (Match match in ResultsRegex.Matches(webpage))
            {
                if (ResultsExclude != null && ResultsExclude.IsMatch(match.Value))
                    continue;
                string urlmatch = ResultUrlRegex.Match(match.Value).Value;
                if (String.IsNullOrEmpty(urlmatch))
                    continue;
                if (ResultUrlRegexReplacement != null)
                    urlmatch = ResultUrlRegexReplacement.Replace(urlmatch, ReplaceUrlWith ?? "");
                string matchUrl = String.Format(ResultUrlLocation, urlmatch);
                if (!FoundUrls.Contains(matchUrl))
                {
                    results.Add(new MetadataLookupSearchResult()
                    {
                        Title = WebSiteHelper.RemoveHTML(ResultTitleRegex.Match(match.Value).Value),
                        Unit = ResultUnitRegex != null && ResultUnitRegex.IsMatch(match.Value) ? WebSiteHelper.RemoveHTML(ResultUnitRegex.Match(match.Value).Value) : "",
                        Url = matchUrl,
                        ThumbnailUrl = ThumbnailRegex != null && ThumbnailRegex.IsMatch(match.Value) ? ThumbnailRegex.Match(match.Value).Value : null
                    });
                    FoundUrls.Add(matchUrl);
                }
            }
            if (results.Count == 0)
                WebSiteHelper.DeleteCacheFile(url); // since no results were found, we should delete the cache file
            return results;
        }
    }
}
